package org.hybridlabs.source.beautifier;

/**
* Copyright 2008 hybrid labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.hybridlabs.file.FileHelper;
import org.hybridlabs.source.beautifier.replacement.DefaultTypeReplacementStrategy;
import org.hybridlabs.source.beautifier.replacement.TypeReplacementStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.hunsicker.jalopy.language.antlr.JavaNode;

/**
 * Simple oAW beautifier implementation based on Jalopy and the antlr JavaNode. The
 * implementation substitutes fully qualified class names (as long as there is
 * no conflict) with the short name and the according import statement.
 *
 * @author Karsten Klein, hybrid labs
 * @author Jan Labrie
 */
public class JavaImportBeautifierImpl extends JavaBeautifier implements BeautifierWithConvention {

    private static final Logger LOG = LoggerFactory.getLogger(JavaImportBeautifierImpl.class);

    private static final String LINESEP = System.getProperty("line.separator");

    private static final CharacterSequence DEFAULT_PACKAGE = new CharacterSequence(new StringBuffer());

    private static Set<Integer> startSequenceTypeSet = new HashSet<Integer>();
    private static Set<Integer> terminateSequenceTypeSet = new HashSet<Integer>();

    static {
        startSequenceTypeSet.add(new Integer(PACKAGE));
        startSequenceTypeSet.add(new Integer(IMPORT_STATEMENT));
        startSequenceTypeSet.add(new Integer(GENERIC_UPPER_BOUNDS));
        startSequenceTypeSet.add(new Integer(EXTENDS_CLAUSE));
        startSequenceTypeSet.add(new Integer(IMPLEMENTS_CLAUSE));
        startSequenceTypeSet.add(new Integer(TYPE));
        startSequenceTypeSet.add(new Integer(EXPRESSION));
        startSequenceTypeSet.add(new Integer(NEW));
        startSequenceTypeSet.add(new Integer(PACKAGE_ANNOTATION));
        startSequenceTypeSet.add(new Integer(ANNOTATION));
        startSequenceTypeSet.add(new Integer(ANNOTATION_VALUE));
        startSequenceTypeSet.add(new Integer(CAST));
        startSequenceTypeSet.add(new Integer(CLASS));
        startSequenceTypeSet.add(new Integer(AT));
        startSequenceTypeSet.add(new Integer(SEMI));
        startSequenceTypeSet.add(new Integer(STATIC_IMPORT_STATEMENT));
        startSequenceTypeSet.add(new Integer(DOT));
        startSequenceTypeSet.add(new Integer(COMMA));
        startSequenceTypeSet.add(new Integer(THROWS));
    }

    static {
        // these terminate a sequence AND inherit the scope of the current (separator types)
        terminateSequenceTypeSet.add(new Integer(10));
        terminateSequenceTypeSet.add(new Integer(11));
        terminateSequenceTypeSet.add(new Integer(39));
        terminateSequenceTypeSet.add(new Integer(45));
        terminateSequenceTypeSet.add(new Integer(DOT));
        terminateSequenceTypeSet.add(new Integer(100));
        terminateSequenceTypeSet.add(new Integer(109));
        terminateSequenceTypeSet.add(new Integer(COMMA));
    }

    private boolean isOrganizeImports = true;
    private boolean isStrict = true;
    private boolean isFormat = true;

    private Map<Integer, TypeReplacementStrategy> strategyMap = new HashMap<Integer, TypeReplacementStrategy>();

    public JavaImportBeautifierImpl() {
        // populate strategy map
    	strategyMap.put(new Integer(NEW), new DefaultTypeReplacementStrategy("[\\,\\<\\)\\[\\<\\(\\s]"));
    	strategyMap.put(new Integer(GENERIC_UPPER_BOUNDS), new DefaultTypeReplacementStrategy( "[\\(\\,\\<\\[\\,\\>\\s]"));
    	strategyMap.put(new Integer(EXTENDS_CLAUSE), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s]"));
    	strategyMap.put(new Integer(IMPLEMENTS_CLAUSE), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s]"));
    	strategyMap.put(new Integer(THROWS), new DefaultTypeReplacementStrategy("[\\(\\,\\>\\<\\)\\.\\s*]"));
    	strategyMap.put(new Integer(TYPE), new DefaultTypeReplacementStrategy("[\\(\\,\\>\\)\\[\\<\\;\\s\\.]"));
    	strategyMap.put(new Integer(EXPRESSION), new DefaultTypeReplacementStrategy("[\\(\\,\\>\\<\\)\\.\\s*]"));
    	strategyMap.put(new Integer(ANNOTATION), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s]"));
    	strategyMap.put(new Integer(AT), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s]"));
    	strategyMap.put(new Integer(CAST), new DefaultTypeReplacementStrategy("[\\(\\)\\>\\<\\[\\,\\;\\{\\s\\.]"));
    	strategyMap.put(new Integer(ANNOTATION_VALUE), new DefaultTypeReplacementStrategy("[\\(\\>\\<\\[\\,\\;\\{\\s\\.]"));
    }

    public boolean isFormat() {
        return isFormat;
    }

    public void setFormat(boolean isFormat) {
        this.isFormat = isFormat;
    }

    public boolean isOrganizeImports() {
        return isOrganizeImports;
    }

    public void setOrganizeImports(boolean isOrganizeImports) {
        this.isOrganizeImports = isOrganizeImports;
    }

    public boolean isStrict() {
        return isStrict;
    }

    public void setStrict(boolean isOrganizeOnWildcards) {
        this.isStrict = isOrganizeOnWildcards;
    }

    public void beautify(CharacterSequence sb) {
        File file = null;
        try {
            file = File.createTempFile("hybridlabs-beautifier", ".java");
            if (isOrganizeImports()) {
                organizeImports(sb, file);
            }
            if (isFormat()) {
                format(sb, file);
            }
        } catch (Exception e) {
        	handleError(e, sb);
        } catch (Error e) {
        	handleError(e, sb);
        } finally {
            if (file != null) {
                file.delete();
            }
        }
    }

	/**
	 * Beautifies the source file and writes the result to the target file.
	 * Source and target may be identical.
	 *
	 * @param sourceFile
	 * @param targetFile
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public void beautify(File sourceFile, File targetFile)
			throws FileNotFoundException, IOException {
		StringBuffer buffer = FileHelper.loadStringBuffer(sourceFile);

		CharacterSequence sequence = new CharacterSequence(buffer);
		beautify(sequence);

		String targetPath = sourceFile.getAbsolutePath();
		if (targetFile != null) {
			targetPath = targetFile.getAbsolutePath();
		}

		FileWriter writer = new FileWriter(new File(targetPath));
		writer.write(sequence.toString());
		writer.close();
	}

	private void handleError (Throwable e, CharacterSequence sb) {
        LOG.error(e.getClass().getSimpleName()+" occured during beautification. Content:" + LINESEP + sb.subSequence(0, Math.min(160, sb.length())) + "...");
        if (LOG.isDebugEnabled()) {
            LOG.debug("Error during beautification.", e);
        } else if (e.getMessage()!=null) {
        	LOG.error ("Error message: "+e.getMessage());
        }
    }

    private void organizeImports(CharacterSequence sequence, File file) {

        // create formatter context for this file
        BeautifierContext formatterContext = new BeautifierContext();

        // use formatter context to traverse the abstract syntax tree
        JavaNode node = createJavaNode(sequence, file);
        traverseAst(node, formatterContext, 0);

        Set<TypeContext> sequences = formatterContext.getSequences();

        // dump all detected sequences to system out in DEBUG mode
        if (LOG.isDebugEnabled()) {
            printSequences(sequences);
        }

        // extract package from ast
        CharacterSequence currentPackage = DEFAULT_PACKAGE;
        for (TypeContext typeContext : sequences) {
            if (typeContext.getType() == PACKAGE_ANNOTATION) {
                currentPackage = typeContext.getQualifiedName();
                break;
            }
        }

        // determine absolute position of package declaration
        int packageEndPos = findPositionInCharacterSequence(sequence,
                formatterContext.getPackageLine(), formatterContext.getPackageEnd());

        // determine absolute position of import declaration
        int importEndPos = findPositionInCharacterSequence(sequence,
                formatterContext.getImportEndLine(), formatterContext.getImportEndColumn());
        // split the file in header (including the package declaration) ...
        CharacterSequence result;
        if (DEFAULT_PACKAGE.equals(currentPackage)) {
            result = new CharacterSequence(new StringBuffer());
            packageEndPos = 0;
        } else {
            result = sequence.subCharacterSequence(0, packageEndPos);
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Package: " + currentPackage + "[" + packageEndPos + "]");
            LOG.debug("Imports end at: " + importEndPos);
        }

        if (importEndPos == -1) {
            importEndPos = packageEndPos;
        }

        CharSequence imports = sequence.subSequence(packageEndPos, importEndPos);
        // do not organize anything if there is a wildcard import - this could lead to ambiguity
        if (isStrict && imports.toString().contains(".*")) {
        	LOG.info("Did not organize import "+file.getAbsolutePath()+" - file contains wildcard imports.");
        } else {
	        // and the body part (imports, comments, classes/interfaces)
	        CharSequence body = sequence.subSequence(importEndPos, sequence.length());

	        // extract the detected imports sequences
	        Set<CharacterSequence> importTypes = extractImports(sequences);

	        // set for monitoring the already replaced types
	        Set<CharacterSequence> replacedSet = new TreeSet<CharacterSequence>();

	        for (TypeContext typeContext : sequences) {
	            if (typeContext.getType() == CLASS) {
	                if (currentPackage == null) {
	                    importTypes.add(typeContext.getQualifiedName());
	                } else {
	                    CharacterSequence sb = new CharacterSequence(new StringBuffer(100));
	                    sb.append(currentPackage);
	                    sb.append('.');
	                    sb.append(typeContext.getQualifiedName());
	                    importTypes.add(sb);
	                }
	            }
	        }

	        body = replaceFullQualifiedClassNames(body, sequences, importTypes, replacedSet);

	        if (imports.length() > 0) {
	            result.append(imports);
	        }

	        // populate header with imports
	        int numNewImports = 0;
	        for (CharacterSequence type : replacedSet) {
	            if (!isRedundant(type, importTypes)) {

	                int index = type.lastIndexOf('.');
	                CharSequence typePackage = index == -1 ? DEFAULT_PACKAGE : type.subSequence(0, index);

	                // make sure the import is not redundant, because it is part of the current package
	                // and also the java.lang package does not need to be imported
	                if (!currentPackage.equals(typePackage) && !typePackage.equals("java.lang")) {
	                    result.append("import ");
	                    result.append(type);
	                    result.append(";");
	                    result.append(LINESEP);
	                    numNewImports += 1;
	                }
	            }
	        }

	        // separate existing imports from added imports
	        if (numNewImports > 0) {
	            result.append(LINESEP);
	        }

	        // supplement the rest of the source
	        result.append(body);

	        sequence.set(result);
        }
    }

    private CharSequence replaceFullQualifiedClassNames(CharSequence body, Set<TypeContext> sequences, Set<CharacterSequence> importTypes, Set<CharacterSequence> replacedSet) {

        // body consists of import, comment, classes etc
        // the existing imports should not be touched by the replacement

        Set<CharacterSequence> conflictCache = new HashSet<CharacterSequence>();

        // At first replace all java.lang imports
        for (TypeContext typeContext : sequences) {
        	if (typeContext.getQualifiedName().startsWith("java.lang")) {
        		body = replaceFullQualifiedClassNames(typeContext, body, importTypes, replacedSet, conflictCache);
        	}
        }

        // And then the 'ordinary' ones
        for (TypeContext typeContext : sequences) {
            body = replaceFullQualifiedClassNames(typeContext, body, importTypes, replacedSet, conflictCache);
        }

        return body;
    }

	private CharSequence replaceFullQualifiedClassNames(
			TypeContext typeContext,
			CharSequence body,
			Set<CharacterSequence> importTypes,
			Set<CharacterSequence> replacedSet,
			Set<CharacterSequence> conflictCache) {

		CharacterSequence type = typeContext.getQualifiedName();

		if (!replacedSet.contains(type)) {
		    if (typeContext.isValidType()) {
		        // ensure there are no conflicts
		        if (!isConflict(type, importTypes, replacedSet, conflictCache)) {
		            // access the replacement strategy
		            TypeReplacementStrategy strategy = (TypeReplacementStrategy)
		                strategyMap.get(new Integer(typeContext.getType()));

		            if (strategy != null) {
		                String replacement = strategy.composeReplace(type);
		                Pattern pattern = Pattern.compile(strategy
		                        .composeMatch(type));
		                Matcher matcher = pattern.matcher(body);

		                // check whether the pattern was matched
		                if (matcher.find()) {
		                    // match and replace the type by the short form (in the sub string)
		                    body = matcher.replaceAll(replacement);

		                    // mark type as replaced
		                    replacedSet.add(type);
		                }
		            }
		        }
		    }
		}
		return body;
	}

    private Set<CharacterSequence> extractImports(Set<TypeContext> sequences) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("These are the existing imports:");
        }
        Set<CharacterSequence> importTypes = new TreeSet<CharacterSequence>();

        for (TypeContext typeContext : sequences) {
            if (typeContext.getType() == IMPORT_STATEMENT) {
                CharacterSequence sb = typeContext.getQualifiedName();
                importTypes.add(sb);
                LOG.debug("{}", sb.toString());
            } else if (typeContext.getType() == STATIC_IMPORT_STATEMENT) {
                CharacterSequence sb = new CharacterSequence(new StringBuffer(200));
                sb.append("static ");
                sb.append(typeContext.getQualifiedName());
                importTypes.add(sb);
                LOG.debug("{}", sb);
            }
        }
        return importTypes;
    }

    private void printSequences(Set<TypeContext> sequences) {
        LOG.debug("These are the symbols that are candidate to be replaced by imports:");
        for (TypeContext typeContext : sequences) {
            LOG.debug("{}", typeContext);
        }
    }

    private boolean isConflict(CharacterSequence type, Set<CharacterSequence> importList, Set<CharacterSequence> replacedSet, Set<CharacterSequence> conflictCache) {
        if (isConflict(type, replacedSet, conflictCache)) {
            return true;
        }

        if (isConflict(type, importList, conflictCache)) {
            return true;
        }
        return false;
    }

    private boolean isConflict(CharacterSequence type, Set<CharacterSequence> testSet, Set<CharacterSequence> conflictCache) {
        if (testSet.contains(type)) {
            return false;
        }

        if (conflictCache.contains(type)) {
            return true;
        }

        for (CharacterSequence importType : testSet) {
            if (!importType.endsWith('*')) {
                if (!type.equals(importType)) {
                    CharSequence t = importType
                            .substring(importType.lastIndexOf('.') + 1);
                    if (type.endsWith("." + t) && !importType.startsWith("static ")) {
                        conflictCache.add(type);
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Method to calculate whether a symbol is already covered by a set of import sequences
     * @param type The type which is checked
     * @param importList The list of import statements in which regard the checking is done
     * @return true when the symbol is covered by the import list
     */
    private final boolean isRedundant(CharacterSequence type, Set<CharacterSequence> importList) {
        for (CharacterSequence importType : importList) {
            if (importType.equals(type) ||
                    importType.endsWith('*') &&
                    notLastPart(type).equals(notLastPart(importType))) {
                return true;
            }
        }
        return false;
    }

    private String notLastPart(CharacterSequence charSeq) {
        String result= "";
        int i = charSeq.lastIndexOf('.');
        if (i >= 0) {
            result =  charSeq.subCharacterSequence(0, i).toString();
        }
        return result;
    }

    private int traverseAst(JavaNode ast, BeautifierContext context, int depth) {

        // print this node
        if (LOG.isDebugEnabled()) {
            preparePrintln(depth);
            LOG.debug("{}", ast);
        }

        if (context.isProcessed(ast)) {
            return -1;
        }

        context.preProcess(ast);

        // check whether this subtree is to be ignored completely
        Integer astType = new Integer(ast.getType());

        if (terminateSequenceTypeSet.contains(astType)) {
            context.terminateCurrentSequence(ast, context.getCurrentTypeContext().getType());
        } else if (startSequenceTypeSet.contains(astType)) {
            context.terminateCurrentSequence(ast, -1);
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("TRAVERSING: " + ast.getType());
        }

        // traverse all children
        JavaNode child = (JavaNode) ast.getFirstChild();
        if (child != null) {
            traverseAst(child, context, depth + 1);
        }

        context.postProcess(ast);


        // print if a type was detected
        if (ast.getType() == IDENT) {
            context.addToCurrentTypeContext(ast);
            if (LOG.isDebugEnabled()) {
                LOG.debug(preparePrintln(depth)+context.getCurrentTypeContext());
            }
        }

        // continue visiting the tree (traverse siblings)
        JavaNode next = (JavaNode) ast.getNextSibling();
        while (next != null) {
            traverseAst(next, context, depth);
            next = (JavaNode) next.getNextSibling();
        }

        return -1;
    }

    private String preparePrintln(int depth) {
    	StringBuilder result = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            result.append("    ");
        }
        return result.toString();
    }

}
